{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Advanced tour of the Bayesian Optimization package"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "from bayes_opt import BayesianOptimization"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1. Suggest-Evaluate-Register Paradigm\n",
    "\n",
    "Internally the `maximize` method is simply a wrapper around the methods `suggest`, `probe`, and `register`. If you need more control over your optimization loops the Suggest-Evaluate-Register paradigm should give you that extra flexibility.\n",
    "\n",
    "For an example of running the `BayesianOptimization` in a distributed fashion (where the function being optimized is evaluated concurrently in different cores/machines/servers), checkout the `async_optimization.py` script in the examples folder."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Let's start by definying our function, bounds, and instanciating an optimization object.\n",
    "def black_box_function(x, y):\n",
    "    return -x ** 2 - (y - 1) ** 2 + 1"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Notice that the evaluation of the blackbox function will NOT be carried out by the optimizer object. We are simulating a situation where this function could be being executed in a different machine, maybe it is written in another language, or it could even be the result of a chemistry experiment. Whatever the case may be, you can take charge of it and as long as you don't invoke the `probe` or `maximize` methods directly, the optimizer object will ignore the blackbox function."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "optimizer = BayesianOptimization(\n",
    "    f=None,\n",
    "    pbounds={'x': (-2, 2), 'y': (-3, 3)},\n",
    "    verbose=2,\n",
    "    random_state=1,\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "One extra ingredient we will need is an `UtilityFunction` instance. In case it is not clear why, take a look at the literature to understand better how this method works."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "from bayes_opt import UtilityFunction\n",
    "\n",
    "utility = UtilityFunction(kind=\"ucb\", kappa=2.5, xi=0.0)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The `suggest` method of our optimizer can be called at any time. What you get back is a suggestion for the next parameter combination the optimizer wants to probe.\n",
    "\n",
    "Notice that while the optimizer hasn't observed any points, the suggestions will be random. However, they will stop being random and improve in quality the more points are observed."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Next point to probe is: {'y': 1.3219469606529488, 'x': -0.331911981189704}\n"
     ]
    }
   ],
   "source": [
    "next_point_to_probe = optimizer.suggest(utility)\n",
    "print(\"Next point to probe is:\", next_point_to_probe)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You are now free to evaluate your function at the suggested point however/whenever you like."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Found the target value to be: 0.7861845912690542\n"
     ]
    }
   ],
   "source": [
    "target = black_box_function(**next_point_to_probe)\n",
    "print(\"Found the target value to be:\", target)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Last thing left to do is to tell the optimizer what target value was observed."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "optimizer.register(\n",
    "    params=next_point_to_probe,\n",
    "    target=target,\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1.1 The maximize loop\n",
    "\n",
    "And that's it. By repeating the steps above you recreate the internals of the `maximize` method. This should give you all the flexibility you need to log progress, hault execution, perform concurrent evaluations, etc."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "-19.0 {'y': -3.0, 'x': 2.0}\n",
      "-12.194801029414048 {'y': -2.412527795983739, 'x': -1.2447710918286998}\n",
      "0.6381713808008993 {'y': 1.4965397889559267, 'x': -0.3395244574146384}\n",
      "0.5052897389362041 {'y': 1.2837707069731576, 'x': -0.6435716330974743}\n",
      "0.9493808230928116 {'y': 1.2241444765020055, 'x': -0.019453291773639306}\n",
      "{'target': 0.9493808230928116, 'params': {'y': 1.2241444765020055, 'x': -0.019453291773639306}}\n"
     ]
    }
   ],
   "source": [
    "for _ in range(5):\n",
    "    next_point = optimizer.suggest(utility)\n",
    "    target = black_box_function(**next_point)\n",
    "    optimizer.register(params=next_point, target=target)\n",
    "    \n",
    "    print(target, next_point)\n",
    "print(optimizer.max)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2. Dealing with discrete parameters\n",
    "\n",
    "**There is no principled way of dealing with discrete parameters using this package.**\n",
    "\n",
    "Ok, now that we got that out of the way, how do you do it? You're bound to be in a situation where some of your function's parameters may only take on discrete values. Unfortunately, the nature of bayesian optimization with gaussian processes doesn't allow for an easy/intuitive way of dealing with discrete parameters - but that doesn't mean it is impossible. The example below showcases a simple, yet reasonably adequate, way to dealing with discrete parameters."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "def func_with_discrete_params(x, y, d):\n",
    "    # Simulate necessity of having d being discrete.\n",
    "    assert type(d) == int\n",
    "    \n",
    "    return ((x + y + d) // (1 + d)) / (1 + (x + y) ** 2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "def function_to_be_optimized(x, y, w):\n",
    "    d = int(w)\n",
    "    return func_with_discrete_params(x, y, d)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "optimizer = BayesianOptimization(\n",
    "    f=function_to_be_optimized,\n",
    "    pbounds={'x': (-10, 10), 'y': (-10, 10), 'w': (0, 5)},\n",
    "    verbose=2,\n",
    "    random_state=1,\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "|   iter    |  target   |     w     |     x     |     y     |\n",
      "-------------------------------------------------------------\n",
      "| \u001b[0m 1       \u001b[0m | \u001b[0m-0.06199 \u001b[0m | \u001b[0m 2.085   \u001b[0m | \u001b[0m 4.406   \u001b[0m | \u001b[0m-9.998   \u001b[0m |\n",
      "| \u001b[95m 2       \u001b[0m | \u001b[95m-0.0344  \u001b[0m | \u001b[95m 1.512   \u001b[0m | \u001b[95m-7.065   \u001b[0m | \u001b[95m-8.153   \u001b[0m |\n",
      "| \u001b[0m 3       \u001b[0m | \u001b[0m-0.2177  \u001b[0m | \u001b[0m 0.9313  \u001b[0m | \u001b[0m-3.089   \u001b[0m | \u001b[0m-2.065   \u001b[0m |\n",
      "| \u001b[95m 4       \u001b[0m | \u001b[95m 0.1865  \u001b[0m | \u001b[95m 2.694   \u001b[0m | \u001b[95m-1.616   \u001b[0m | \u001b[95m 3.704   \u001b[0m |\n",
      "| \u001b[0m 5       \u001b[0m | \u001b[0m-0.2187  \u001b[0m | \u001b[0m 1.022   \u001b[0m | \u001b[0m 7.562   \u001b[0m | \u001b[0m-9.452   \u001b[0m |\n",
      "| \u001b[0m 6       \u001b[0m | \u001b[0m 0.009975\u001b[0m | \u001b[0m 5.0     \u001b[0m | \u001b[0m 10.0    \u001b[0m | \u001b[0m 10.0    \u001b[0m |\n",
      "| \u001b[0m 7       \u001b[0m | \u001b[0m 0.0     \u001b[0m | \u001b[0m 5.0     \u001b[0m | \u001b[0m-10.0    \u001b[0m | \u001b[0m 10.0    \u001b[0m |\n",
      "| \u001b[0m 8       \u001b[0m | \u001b[0m 0.09003 \u001b[0m | \u001b[0m 0.0     \u001b[0m | \u001b[0m 0.4916  \u001b[0m | \u001b[0m 10.0    \u001b[0m |\n",
      "| \u001b[0m 9       \u001b[0m | \u001b[0m-0.007481\u001b[0m | \u001b[0m 5.0     \u001b[0m | \u001b[0m-10.0    \u001b[0m | \u001b[0m-10.0    \u001b[0m |\n",
      "| \u001b[0m 10      \u001b[0m | \u001b[0m 0.01989 \u001b[0m | \u001b[0m 5.0     \u001b[0m | \u001b[0m-0.02203 \u001b[0m | \u001b[0m 10.0    \u001b[0m |\n",
      "| \u001b[0m 11      \u001b[0m | \u001b[0m 0.0189  \u001b[0m | \u001b[0m 5.0     \u001b[0m | \u001b[0m 10.0    \u001b[0m | \u001b[0m 0.238   \u001b[0m |\n",
      "| \u001b[0m 12      \u001b[0m | \u001b[0m-0.2149  \u001b[0m | \u001b[0m 0.0     \u001b[0m | \u001b[0m-10.0    \u001b[0m | \u001b[0m 5.282   \u001b[0m |\n",
      "| \u001b[0m 13      \u001b[0m | \u001b[0m 0.05995 \u001b[0m | \u001b[0m 0.0     \u001b[0m | \u001b[0m 10.0    \u001b[0m | \u001b[0m 5.786   \u001b[0m |\n",
      "| \u001b[0m 14      \u001b[0m | \u001b[0m-0.01299 \u001b[0m | \u001b[0m 5.0     \u001b[0m | \u001b[0m-2.367   \u001b[0m | \u001b[0m-10.0    \u001b[0m |\n",
      "| \u001b[0m 15      \u001b[0m | \u001b[0m 0.03637 \u001b[0m | \u001b[0m 5.0     \u001b[0m | \u001b[0m 3.773   \u001b[0m | \u001b[0m 3.575   \u001b[0m |\n",
      "| \u001b[0m 16      \u001b[0m | \u001b[0m-0.01214 \u001b[0m | \u001b[0m 5.0     \u001b[0m | \u001b[0m-10.0    \u001b[0m | \u001b[0m 0.9779  \u001b[0m |\n",
      "| \u001b[0m 17      \u001b[0m | \u001b[0m 0.0     \u001b[0m | \u001b[0m 5.0     \u001b[0m | \u001b[0m 10.0    \u001b[0m | \u001b[0m-10.0    \u001b[0m |\n",
      "| \u001b[0m 18      \u001b[0m | \u001b[0m 0.0     \u001b[0m | \u001b[0m 5.0     \u001b[0m | \u001b[0m-4.58    \u001b[0m | \u001b[0m 5.518   \u001b[0m |\n",
      "| \u001b[0m 19      \u001b[0m | \u001b[0m-0.04988 \u001b[0m | \u001b[0m 0.0     \u001b[0m | \u001b[0m-10.0    \u001b[0m | \u001b[0m-10.0    \u001b[0m |\n",
      "| \u001b[0m 20      \u001b[0m | \u001b[0m 0.1246  \u001b[0m | \u001b[0m 0.0     \u001b[0m | \u001b[0m 2.311   \u001b[0m | \u001b[0m 5.116   \u001b[0m |\n",
      "| \u001b[0m 21      \u001b[0m | \u001b[0m 0.04988 \u001b[0m | \u001b[0m 0.0     \u001b[0m | \u001b[0m 10.0    \u001b[0m | \u001b[0m 10.0    \u001b[0m |\n",
      "| \u001b[0m 22      \u001b[0m | \u001b[0m 0.04567 \u001b[0m | \u001b[0m 2.029   \u001b[0m | \u001b[0m 0.1434  \u001b[0m | \u001b[0m 6.398   \u001b[0m |\n",
      "| \u001b[0m 23      \u001b[0m | \u001b[0m 0.0     \u001b[0m | \u001b[0m 5.0     \u001b[0m | \u001b[0m 4.685   \u001b[0m | \u001b[0m-4.937   \u001b[0m |\n",
      "| \u001b[0m 24      \u001b[0m | \u001b[0m 0.06466 \u001b[0m | \u001b[0m 0.0     \u001b[0m | \u001b[0m 5.198   \u001b[0m | \u001b[0m 10.0    \u001b[0m |\n",
      "| \u001b[95m 25      \u001b[0m | \u001b[95m 0.3751  \u001b[0m | \u001b[95m 5.0     \u001b[0m | \u001b[95m-0.6795  \u001b[0m | \u001b[95m 1.97    \u001b[0m |\n",
      "| \u001b[0m 26      \u001b[0m | \u001b[0m 0.0     \u001b[0m | \u001b[0m 5.0     \u001b[0m | \u001b[0m-2.001   \u001b[0m | \u001b[0m-0.5515  \u001b[0m |\n",
      "| \u001b[0m 27      \u001b[0m | \u001b[0m 0.1072  \u001b[0m | \u001b[0m 0.0     \u001b[0m | \u001b[0m 10.0    \u001b[0m | \u001b[0m-1.419   \u001b[0m |\n",
      "| \u001b[0m 28      \u001b[0m | \u001b[0m-0.08895 \u001b[0m | \u001b[0m 0.0     \u001b[0m | \u001b[0m-2.048   \u001b[0m | \u001b[0m-10.0    \u001b[0m |\n",
      "| \u001b[0m 29      \u001b[0m | \u001b[0m 0.1907  \u001b[0m | \u001b[0m 0.0     \u001b[0m | \u001b[0m 3.994   \u001b[0m | \u001b[0m-0.1557  \u001b[0m |\n",
      "| \u001b[0m 30      \u001b[0m | \u001b[0m-0.0     \u001b[0m | \u001b[0m 0.0     \u001b[0m | \u001b[0m-10.0    \u001b[0m | \u001b[0m 10.0    \u001b[0m |\n",
      "=============================================================\n"
     ]
    }
   ],
   "source": [
    "optimizer.maximize(alpha=1e-3)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3. Tuning the underlying Gaussian Process\n",
    "\n",
    "The bayesian optimization algorithm works by performing a gaussian process regression of the observed combination of parameters and their associated target values. The predicted parameter$\\rightarrow$target hyper-surface (and its uncertainty) is then used to guide the next best point to probe."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3.1 Passing parameter to the GP\n",
    "\n",
    "Depending on the problemn it could be beneficial to change the default parameters of the underlying GP. You can simply pass GP parameters to the maximize method directly as you can see below:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "|   iter    |  target   |     x     |     y     |\n",
      "-------------------------------------------------\n",
      "| \u001b[0m 1       \u001b[0m | \u001b[0m 0.7862  \u001b[0m | \u001b[0m-0.3319  \u001b[0m | \u001b[0m 1.322   \u001b[0m |\n",
      "| \u001b[0m 2       \u001b[0m | \u001b[0m-18.96   \u001b[0m | \u001b[0m 1.993   \u001b[0m | \u001b[0m-2.998   \u001b[0m |\n",
      "| \u001b[0m 3       \u001b[0m | \u001b[0m 0.7858  \u001b[0m | \u001b[0m-0.3333  \u001b[0m | \u001b[0m 1.321   \u001b[0m |\n",
      "| \u001b[0m 4       \u001b[0m | \u001b[0m 0.5787  \u001b[0m | \u001b[0m-0.429   \u001b[0m | \u001b[0m 1.487   \u001b[0m |\n",
      "| \u001b[0m 5       \u001b[0m | \u001b[0m 0.7798  \u001b[0m | \u001b[0m 0.02543 \u001b[0m | \u001b[0m 1.469   \u001b[0m |\n",
      "| \u001b[95m 6       \u001b[0m | \u001b[95m 0.9779  \u001b[0m | \u001b[95m 0.1301  \u001b[0m | \u001b[95m 0.9282  \u001b[0m |\n",
      "=================================================\n"
     ]
    }
   ],
   "source": [
    "optimizer = BayesianOptimization(\n",
    "    f=black_box_function,\n",
    "    pbounds={'x': (-2, 2), 'y': (-3, 3)},\n",
    "    verbose=2,\n",
    "    random_state=1,\n",
    ")\n",
    "optimizer.maximize(\n",
    "    init_points=1,\n",
    "    n_iter=5,\n",
    "    # What follows are GP regressor parameters\n",
    "    alpha=1e-3,\n",
    "    n_restarts_optimizer=5\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Another alternative, specially useful if you're calling `maximize` multiple times or optimizing outside the `maximize` loop, is to call the `set_gp_params` method."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [],
   "source": [
    "optimizer.set_gp_params(normalize_y=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3.2 Tuning the `alpha` parameter\n",
    "\n",
    "When dealing with functions with discrete parameters,or particularly erratic target space it might be beneficial to increase the value of the `alpha` parameter. This parameters controls how much noise the GP can handle, so increase it whenever you think that extra flexibility is needed."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3.3 Changing kernels\n",
    "\n",
    "By default this package uses the Mattern 2.5 kernel. Depending on your use case you may find that tunning the GP kernel could be beneficial. You're on your own here since these are very specific solutions to very specific problems."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Observers Continued\n",
    "\n",
    "Observers are objects that subscribe and listen to particular events fired by the `BayesianOptimization` object. \n",
    "\n",
    "When an event gets fired a callback function is called with the event and the `BayesianOptimization` instance passed as parameters. The callback can be specified at the time of subscription. If none is given it will look for an `update` method from the observer."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [],
   "source": [
    "from bayes_opt.event import DEFAULT_EVENTS, Events"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [],
   "source": [
    "optimizer = BayesianOptimization(\n",
    "    f=black_box_function,\n",
    "    pbounds={'x': (-2, 2), 'y': (-3, 3)},\n",
    "    verbose=2,\n",
    "    random_state=1,\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [],
   "source": [
    "class BasicObserver:\n",
    "    def update(self, event, instance):\n",
    "        \"\"\"Does whatever you want with the event and `BayesianOptimization` instance.\"\"\"\n",
    "        print(\"Event `{}` was observed\".format(event))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [],
   "source": [
    "my_observer = BasicObserver()\n",
    "\n",
    "optimizer.subscribe(\n",
    "    event=Events.OPTIMIZATION_STEP,\n",
    "    subscriber=my_observer,\n",
    "    callback=None, # Will use the `update` method as callback\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Alternatively you have the option to pass a completely different callback."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [],
   "source": [
    "def my_callback(event, instance):\n",
    "    print(\"Go nuts here!\")\n",
    "\n",
    "optimizer.subscribe(\n",
    "    event=Events.OPTIMIZATION_START,\n",
    "    subscriber=\"Any hashable object\",\n",
    "    callback=my_callback,\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Go nuts here!\n",
      "Event `optimization:step` was observed\n",
      "Event `optimization:step` was observed\n",
      "Event `optimization:step` was observed\n"
     ]
    }
   ],
   "source": [
    "optimizer.maximize(init_points=1, n_iter=2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "For a list of all default events you can checkout `DEFAULT_EVENTS`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "['optimization:start', 'optimization:step', 'optimization:end']"
      ]
     },
     "execution_count": 20,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "DEFAULT_EVENTS"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.5.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
